package org.quincy.rock.core.dao;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.quincy.rock.core.dao.annotation.MoreToOne;
import org.quincy.rock.core.dao.annotation.OneToMore;
import org.quincy.rock.core.dao.annotation.OneToOne;
import org.quincy.rock.core.dao.annotation.Temporary;
import org.quincy.rock.core.dao.sql.Predicate;
import org.quincy.rock.core.dao.sql.Sort;
import org.quincy.rock.core.function.RtFunction;
import org.quincy.rock.core.util.StringUtil;

/**
 * <b>SQL语句提供者。</b>
 * <p><b>详细说明：</b></p>
 * <!-- 在此添加详细说明 -->
 * 根据实体类信息生成SQL语句。
 * @version 1.0
 * @author wks
 * @since 1.0
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class SQLProvider<CTX> {
	public static final String ENTITY_TYPE_VAR_NAME = "entityType";
	public static final String ENTITY_VAR_NAME = "entity";
	public static final String VO_MAP_VAR_NAME = "voMap";
	public static final String ID_VAR_NAME = "id";
	public static final String NAME_VAR_NAME = "name";
	public static final String VALUE_VAR_NAME = "value";
	public static final String IGNORE_ID_VAR_NAME = "ignoreId";
	public static final String IGNORE_NULL_VALUE_VAR_NAME = "ignoreNullValue";
	public static final String WHERE_VAR_NAME = "where";
	public static final String SORT_VAR_NAME = "sort";
	public static final String EXCLUDED_VAR_NAME = "excluded";

	private final Map<String, EntityMetadata> entityMataMap = new HashMap<>();

	/**
	 * 表名或列名转换器。
	 */
	private NameInterceptor nameConverter = NameInterceptor.NOOP;

	/**
	 * 引号Getter。
	 */
	private final RtFunction<CTX, String> quoteGetter;

	/**
	 * 实体类型Getter。
	 */
	private final RtFunction<CTX, Class<?>> entityTypeGetter;

	/**
	 * <b>构造方法。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 */
	public SQLProvider() {
		this(null, null);
	}

	/**
	 * <b>构造方法。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 */
	public SQLProvider(RtFunction<CTX, Class<?>> entityTypeGetter) {
		this(entityTypeGetter, null);
	}

	/**
	 * <b>构造方法。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param entityTypeGetter 实体类型Getter
	 * @param quoteGetter 引号Getter
	 */
	public SQLProvider(RtFunction<CTX, Class<?>> entityTypeGetter, RtFunction<CTX, String> quoteGetter) {
		this.entityTypeGetter = entityTypeGetter;
		this.quoteGetter = quoteGetter;
	}

	public NameInterceptor getNameConverter() {
		return nameConverter;
	}

	public void setNameConverter(NameInterceptor nameConverter) {
		this.nameConverter = nameConverter;
	}

	public final StringBuilder getSelectSQL(CTX ctx, EntityMetadata meta, String... excluded) {
		StringBuilder sql = DaoUtil.getSelectSQL(meta, false, (alias, name, isObj, isCol) -> surround(ctx, alias, name),
				excluded);
		return sql;
	}

	/**
	 * <b>使用引号包裹标识符。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param ctx 环境上下文
	 * @param alias 表别名，针对name是列名的情况，可以为null
	 * @param name 表名或列名
	 * @return 被引号包裹的标识符
	 */
	public final CharSequence surround(CTX ctx, CharSequence alias, CharSequence name) {
		StringBuilder buffer = new StringBuilder();
		if (StringUtils.isNotBlank(alias))
			buffer.append(alias).append(StringUtil.CHAR_DOT);
		String quote = StringUtil.EMPTY;
		if (quoteGetter != null && ctx != null) {
			quote = quoteGetter.apply(ctx);
			if (quote == null || StringUtils.startsWith(name, quote))
				quote = StringUtil.EMPTY;
		}
		buffer.append(quote).append(name).append(quote);
		return buffer;
	}

	public final Class<?> getEntityType(CTX ctx, Map<String, Object> paramMap) {
		Class<?> clazz = getParamValue(paramMap, ENTITY_TYPE_VAR_NAME, "param1");
		if (clazz == null && entityTypeGetter != null && ctx != null)
			clazz = entityTypeGetter.apply(ctx);
		return clazz;
	}

	/**
	 * <b>获得实体元数据。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param clazz 实体类型
	 * @return 实体元数据
	 */
	public EntityMetadata getEntityMetadata(Class<?> clazz) {
		return getEntityMetadata0(clazz, Temporary.class, OneToOne.class, OneToMore.class, MoreToOne.class);
	}

	public String delete(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		String idVarName = getParamName(paramMap, ID_VAR_NAME, "param2");
		EntityMetadata meta = getEntityMetadata(clazz);
		String pkName = meta.pKName();
		StringBuilder sb = new StringBuilder("delete from ").append(surround(ctx, null, meta.tableName()))
				.append(" where ").append(surround(ctx, null, meta.columnName(pkName))).append("=#{").append(idVarName)
				.append('}');
		return sb.toString();
	}

	public String deleteMore(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Iterator<? extends Number> it = ((Iterable) getParamValue(paramMap, "ids", "param2")).iterator();
		EntityMetadata meta = getEntityMetadata(clazz);
		String pkName = meta.pKName();
		StringBuilder sql = new StringBuilder("delete from ").append(surround(ctx, null, meta.tableName()))
				.append(" where ").append(DaoUtil.whereInById(surround(ctx, null, meta.columnName(pkName)), it));
		return sql.toString();
	}

	public String deleteAll(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Predicate where = getParamValue(paramMap, WHERE_VAR_NAME, "param2");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = new StringBuilder("delete from ").append(surround(ctx, null, meta.tableName()));
		if (where != null) {
			sql.append(" where ").append(DaoUtil.whereString(meta, where, false,
					(alias, colName, isObj, isCol) -> surround(ctx, alias, colName), paramMap, null, "#{", "}"));
		}
		return sql.toString();
	}

	public String count(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Predicate where = getParamValue(paramMap, WHERE_VAR_NAME, "param2");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = new StringBuilder("select count(0) ");
		meta.entityTable().build(sql, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
		if (!Predicate.isEmpty(where)) {
			sql.append(" where ").append(DaoUtil.whereString(meta, where, false,
					(alias, colName, isObj, isCol) -> surround(ctx, alias, colName), paramMap, null, "#{", "}"));
		}
		return sql.toString();
	}

	public String findOne(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		String idVarName = getParamName(paramMap, ID_VAR_NAME, "param2");
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param3");
		EntityMetadata meta = getEntityMetadata(clazz);
		String pkColName = meta.columnName(meta.pKName(),
				(alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
		StringBuilder sql = getSelectSQL(ctx, meta, excluded).append(" where ").append(pkColName).append("=#{")
				.append(idVarName).append('}');
		return sql.toString();
	}

	public String findMore(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Iterator<? extends Number> it = ((Iterable) getParamValue(paramMap, "ids", "param2")).iterator();
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param3");
		EntityMetadata meta = getEntityMetadata(clazz);
		String pkColName = meta.columnName(meta.pKName(),
				(alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
		StringBuilder sql = getSelectSQL(ctx, meta, excluded).append(" where ")
				.append(DaoUtil.whereInById(pkColName, it));
		return sql.toString();
	}

	public String findAll(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Predicate where = getParamValue(paramMap, WHERE_VAR_NAME, "param2");
		Sort sort = getParamValue(paramMap, SORT_VAR_NAME, "param3");
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param4");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = getSelectSQL(ctx, meta, excluded);
		if (!Predicate.isEmpty(where)) {
			sql.append(" where ").append(DaoUtil.whereString(meta, where, true,
					(alias, colName, isObj, isCol) -> surround(ctx, alias, colName), paramMap, null, "#{", "}"));
		}
		if (Sort.isSorted(sort)) {
			sql.append(" order by ");
			sort.build(sql, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
		}
		return sql.toString();
	}

	public String findByName(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		String name = getParamValue(paramMap, NAME_VAR_NAME, "param2");
		String valueVarName = getParamName(paramMap, VALUE_VAR_NAME, "param3");
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param4");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = getSelectSQL(ctx, meta, excluded).append(" where ")
				.append(meta.columnName(name, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName)))
				.append("=#{").append(valueVarName).append('}');
		return sql.toString();
	}

	public String findAllByName(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		String name = getParamValue(paramMap, NAME_VAR_NAME, "param2");
		String valueVarName = getParamName(paramMap, VALUE_VAR_NAME, "param3");
		Sort sort = getParamValue(paramMap, SORT_VAR_NAME, "param4");
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param5");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = getSelectSQL(ctx, meta, excluded).append(" where ")
				.append(meta.columnName(name, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName)))
				.append("=#{").append(valueVarName).append('}');
		if (Sort.isSorted(sort)) {
			sql.append(" order by ");
			sort.build(sql, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
		}
		return sql.toString();
	}

	public String exists(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		String idVarName = getParamName(paramMap, ID_VAR_NAME, "param2");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sb = new StringBuilder("select count(0) from ").append(surround(ctx, null, meta.tableName()))
				.append(" where ").append(surround(ctx, null, meta.columnName(meta.pKName()))).append("=#{")
				.append(idVarName).append('}');
		return sb.toString();
	}

	public String existsByName(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		String name = getParamValue(paramMap, NAME_VAR_NAME, "param2");
		String valueVarName = getParamName(paramMap, VALUE_VAR_NAME, "param3");
		String ignoreIdVarName = getParamName(paramMap, IGNORE_ID_VAR_NAME, "param4");
		boolean ignoreId = getParamValue(paramMap, IGNORE_ID_VAR_NAME, "param4") != null;
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sb = new StringBuilder("select count(0) ");
		if (meta.notJoinTableField(name))
			sb.append("FROM ").append(surround(ctx, null, meta.tableName())).append(" where ")
					.append(surround(ctx, null, meta.columnName(name)));
		else {
			meta.entityTable().build(sb, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
			sb.append(" where ")
					.append(meta.columnName(name, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName)));
		}
		sb.append("=#{").append(valueVarName).append('}');
		if (ignoreId) {
			sb.append(" and ").append(surround(ctx, null, meta.columnName(meta.pKName()))).append("!=#{")
					.append(ignoreIdVarName).append('}');
		}
		return sb.toString();
	}

	public String existsByWhere(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Predicate where = getParamValue(paramMap, WHERE_VAR_NAME, "param2");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = new StringBuilder("select count(0) ");
		meta.entityTable().build(sql, (alias, colName, isObj, isCol) -> surround(ctx, alias, colName));
		if (where != null) {
			sql.append(" where ").append(DaoUtil.whereString(meta, where, true,
					(alias, colName, isObj, isCol) -> surround(ctx, alias, colName), paramMap, null, "#{", "}"));
		}
		return sql.toString();
	}

	public String insert(CTX ctx, Map<String, Object> paramMap) {
		Object vo = getParamValue(paramMap, ENTITY_VAR_NAME, "param1");
		String voVarName = getParamName(paramMap, ENTITY_VAR_NAME, "param1");
		boolean ignoreNullValue = getParamValue(paramMap, IGNORE_NULL_VALUE_VAR_NAME, "param2");
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param3");
		EntityMetadata meta = getEntityMetadata(vo.getClass());
		StringBuilder sb = DaoUtil.getInsertSQL(
				meta, vo, ignoreNullValue, (name) -> new StringBuilder("#{").append(voVarName)
						.append(StringUtil.CHAR_DOT).append(name).append('}'),
				(alias, name, isObj, isCol) -> surround(ctx, alias, name), excluded);
		return sb.toString();
	}

	public String update(CTX ctx, Map<String, Object> paramMap) {
		Object vo = getParamValue(paramMap, ENTITY_VAR_NAME, "param1");
		String voVarName = getParamName(paramMap, ENTITY_VAR_NAME, "param1");
		boolean ignoreNullValue = getParamValue(paramMap, IGNORE_NULL_VALUE_VAR_NAME, "param2");
		Predicate where = getParamValue(paramMap, WHERE_VAR_NAME, "param3");
		String[] excluded = getParamValue(paramMap, EXCLUDED_VAR_NAME, "param4");
		EntityMetadata meta = getEntityMetadata(vo.getClass());
		StringBuilder sql = DaoUtil.getUpdateSQL(
				meta, vo, ignoreNullValue, true, (name) -> new StringBuilder("#{").append(voVarName)
						.append(StringUtil.CHAR_DOT).append(name).append('}'),
				(alias, name, isObj, isCol) -> surround(ctx, alias, name), excluded);
		sql.append(" where ");
		if (Predicate.isEmpty(where)) {
			String pkName = meta.pKName();
			sql.append(surround(ctx, null, meta.columnName(pkName))).append("=#{").append(voVarName)
					.append(StringUtil.CHAR_DOT).append(pkName).append('}');
		} else {
			sql.append(DaoUtil.whereString(meta, where, false,
					(alias, colName, isObj, isCol) -> surround(ctx, alias, colName), paramMap, null, "#{", "}"));
		}
		return sql.toString();
	}

	public String updateSpecific(CTX ctx, Map<String, Object> paramMap) {
		Class clazz = getEntityType(ctx, paramMap);
		Map<String, Object> partMap = getParamValue(paramMap, VO_MAP_VAR_NAME, "param2");
		String voMapVarName = getParamName(paramMap, VO_MAP_VAR_NAME, "param2");
		Predicate where = getParamValue(paramMap, WHERE_VAR_NAME, "param3");
		EntityMetadata meta = getEntityMetadata(clazz);
		StringBuilder sql = new StringBuilder("update ").append(surround(ctx, null, meta.tableName())).append(" set ");
		String pkName = meta.pKName();
		for (String name : partMap.keySet()) {
			if (!name.equals(pkName)) {
				sql.append(surround(ctx, null, meta.columnName(name))).append("=#{").append(voMapVarName)
						.append(StringUtil.CHAR_DOT).append(name).append("},");
			}
		}
		sql.setLength(sql.length() - 1);
		sql.append(" where ");
		if (Predicate.isEmpty(where)) {
			sql.append(surround(ctx, null, meta.columnName(pkName))).append("=#{").append(voMapVarName)
					.append(StringUtil.CHAR_DOT).append(pkName).append('}');
		} else {
			sql.append(DaoUtil.whereString(meta, where, false,
					(alias, colName, isObj, isCol) -> surround(ctx, alias, colName), paramMap, null, "#{", "}"));
		}
		return sql.toString();
	}

	protected final EntityMetadata getEntityMetadata0(Class<?> clazz, Class<? extends Annotation>... transientClasses) {
		String key;
		if (ArrayUtils.isEmpty(transientClasses)) {
			key = clazz.getName();
		} else {
			StringBuilder sb = new StringBuilder(clazz.getName());
			for (Class<? extends Annotation> transientClass : transientClasses) {
				sb.append(StringUtil.CHAR_COLON);
				sb.append(transientClass.getSimpleName());
			}
			key = sb.toString();
		}
		EntityMetadata meta = entityMataMap.get(key);
		if (meta == null) {
			EntityMetadataEx tmp = new EntityMetadataEx(clazz, transientClasses);
			tmp.setDefaultNameConverter(nameConverter);
			entityMataMap.put(key, tmp);
			meta = tmp;
		}
		return meta;
	}

	public static <T> T getParamValue(Map paramMap, String key1, String key2) {
		Object value = paramMap.containsKey(key1) ? paramMap.get(key1)
				: paramMap.containsKey(key2) ? paramMap.get(key2) : null;
		return (T) value;
	}

	public static String getParamName(Map paramMap, String key1, String key2) {
		return paramMap.containsKey(key1) ? key1 : paramMap.containsKey(key2) ? key2 : null;
	}
}
